//! # 使用说明
//! 
//! ## 1. 列举桶内对象列表
//! 
//! ```rust
//! use obs_sdk::ObsClient;
//! 
//! static ENDPOINT: &str = "obs.cn-north-4.myhuaweicloud.com";
//! static AK: &str = "YOUR_AK";
//! static SK: &str = "YOUR_SK";
//! static BUCKET_NAME: &str = "bucket_name";
//! 
//! #[tokio::test]
//! async fn test_list_prefix() -> Result<(), Box<dyn std::error::Error>> {
//!     let client = ObsClient {
//!         endpoint: ENDPOINT.to_string(),
//!         ak: AK.to_string(),
//!         sk: SK.to_string(),
//!         bucket: BUCKET_NAME.to_string(),
//!     };
//!     let res = client.list("tmp").await?;
//!     println!("{:?}", res);
//!     Ok(())
//! }
//! ```
//! 
//! ## 2. 上传对象到桶
//! 
//! ```rust
//! use obs_sdk::ObsClient;
//! 
//! static ENDPOINT: &str = "obs.cn-north-4.myhuaweicloud.com";
//! static AK: &str = "YOUR_AK";
//! static SK: &str = "YOUR_SK";
//! static BUCKET_NAME: &str = "bucket_name";
//! 
//! #[tokio::test]
//! async fn test_upload_object() -> Result<(), Box<dyn std::error::Error>> {
//!     let client = ObsClient {
//!         endpoint: ENDPOINT.to_string(),
//!         ak: AK.to_string(),
//!         sk: SK.to_string(),
//!         bucket: BUCKET_NAME.to_string(),
//!     };
//!     let res = client.upload_file("tmp_cargo.txt", "Cargo.txt").await?;
//!     println!("{:?}", res);
//!     Ok(())
//! }
//! ```
//! 
//! ## 3. 下载对象到本地目录
//! 
//! ```rust
//! use obs_sdk::ObsClient;
//! 
//! static ENDPOINT: &str = "obs.cn-north-4.myhuaweicloud.com";
//! static AK: &str = "YOUR_AK";
//! static SK: &str = "YOUR_SK";
//! static BUCKET_NAME: &str = "bucket_name";
//! 
//! #[tokio::test]
//! async fn test_download_file02() -> Result<(), Box<dyn std::error::Error>> {
//!     let client = ObsClient {
//!         endpoint: ENDPOINT.to_string(),
//!         ak: AK.to_string(),
//!         sk: SK.to_string(),
//!         bucket: BUCKET_NAME.to_string(),
//!     };
//!     let res = client.download_file("2hls_stutter-10.mp4", "video/2hls_stutter-10.mp4", false).await;
//!     res
//! }
//! ```
//! 
//! ## 4. 下载对象为字节内容
//! 
//! ```rust
//! use obs_sdk::ObsClient;
//! 
//! static ENDPOINT: &str = "obs.cn-north-4.myhuaweicloud.com";
//! static AK: &str = "YOUR_AK";
//! static SK: &str = "YOUR_SK";
//! static BUCKET_NAME: &str = "bucket_name";
//! 
//! #[tokio::test]
//! async fn test_download_file01() -> Result<(), Box<dyn std::error::Error>> {
//!     let client = ObsClient {
//!         endpoint: ENDPOINT.to_string(),
//!         ak: AK.to_string(),
//!         sk: SK.to_string(),
//!         bucket: BUCKET_NAME.to_string(),
//!     };
//!     let data = client.download_object("2hls_stutter-10.mp4").await?;
//!     let file_path = Path::new("output.mp4");
//!     match fs::write(file_path, data) {
//!         Ok(_) => println!("文件保存成功{:?}", file_path),
//!         Err(e) => eprintln!("文件保存失败:{}", e)
//!     }
//!     Ok(())
//! }
//! ```
//! 
//! ## 5. url鉴权
//! 
//! ```rust
//! use obs_sdk::ObsClient;
//!
//! static ENDPOINT: &str = "obs.cn-north-4.myhuaweicloud.com";
//! static AK: &str = "YOUR_AK";
//! static SK: &str = "YOUR_SK";
//! static BUCKET_NAME: &str = "bucket_name";
//! 
//! #[test]
//! fn test_url_sign() -> Result<(), Box<dyn std::error::Error>> {
//!     let client = ObsClient {
//!         endpoint: ENDPOINT.to_string(),
//!         ak: AK.to_string(),
//!         sk: SK.to_string(),
//!         bucket: BUCKET_NAME.to_string(),
//!     };
//!     let sign_url = client.url_sign("https://ranfs.obs.cn-north-4.myhuaweicloud.com/tmp_cargo.txt")?;
//!     println!("sign_url = {}", sign_url);
//!     Ok(())
//! }
//! ```
//! 
mod utils;
mod algorithm;

use algorithm::HmacSha1;
use std::fs;
use std::path::Path;
use std::io::{Error, ErrorKind};
use serde::{Serialize, Deserialize};
use regex::Regex;
use std::vec::Vec;
use url::{Url, form_urlencoded};
use chrono::Local;
use reqwest::StatusCode;
use urlencoding::encode;

/// 华为云OBS客户端
/// 
pub struct ObsClient {
    pub endpoint: String,
    pub ak: String,
    pub sk: String,
    pub bucket: String,
}

impl ObsClient {

    /// 列举指定前缀开头的所有对象元数据，方法内部会构造http请求：
    /// ```plain
    /// GET / HTTP/1.1
    /// Host: bucketname.obs.cn-north-4.myhuaweicloud.com
    /// Date: date
    /// Authorization: authorization
    /// ```
    /// 
    pub async fn list(&self, prefix: &str) -> Result<Vec<ObjectMeta>, Box<dyn std::error::Error>> {
        // 构造完整的url地址
        let url = format!("https://{}.{}/?prefix={}", self.bucket, self.endpoint, prefix);

        //  获取GMT格式的时间字符串
        let date = utils::now_str_gmt();

        // 创建HmacSha1对象
        let hmacsha1 = HmacSha1();

        // 构造签名用的原始字符串
        let string_to_sign = hmacsha1.header_string_to_sign("GET", "", "", &date, "",  &format!("/{}/", self.bucket));

        // 根据原始字符串+ak，获取header签名
        let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);

        // 构造请求头Authorization的值
        let authorization = format!("OBS {}:{}", self.ak, signature);
        // 构造http请求
        let client = reqwest::Client::new();
        let res = client.get(url)
            .header("Date", &date)
            .header("Authorization", &authorization)
            .send()
            .await?;

        // 如果请求成功，则返回字节内容
        if res.status().is_success() {
            let xml_content_string = res.text().await?;
            let results = XmlParser::new(&xml_content_string).parse();
            return Ok(results);
        }

        Err(Box::new(Error::new(ErrorKind::Other, format!("请求失败，状态码={}", res.status()))))
    }

    /// 上传对象
    /// 
    /// 方法内部构建请求
    /// ```plain
    /// PUT /object01 HTTP/1.1
    /// User-Agent: curl/7.29.0
    /// Host: examplebucket.obs.cn-north-4.myhuaweicloud.com
    /// Accept: */*
    /// Date: WED, 01 Jul 2015 04:11:15 GMT
    /// Authorization: OBS H4IPJX0TQTHTHEBQQCEC:gYqplLq30dEX7GMi2qFWyjdFsyw=
    /// Content-Length: 10240
    /// Expect: 100-continue
    /// 
    /// [1024 Byte data content]
    /// ```
    /// 
    pub async fn upload_object(&self, obj_key: &str, data: Vec<u8>) -> Result<(), Box<dyn std::error::Error>> {
        let obj_key = &Self::urlencode(obj_key);
        // 构造完整的url地址
        let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);

        let md5_string = utils::base64_md5_str(&data);

        //  获取GMT格式的时间字符串
        let date = utils::now_str_gmt();

        // 创建HmacSha1对象
        let hmacsha1 = HmacSha1();

        let file_type = &utils::get_mime_type_from_extension(obj_key)
            .unwrap_or(String::from("application/octet-stream"));

        // 构造签名用的原始字符串
        let string_to_sign = hmacsha1.header_string_to_sign("PUT", &md5_string, file_type, &date, "",  &format!("/{}/{}", self.bucket, obj_key));

        // 根据原始字符串+ak，获取header签名
        let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);

        // 构造请求头Authorization的值
        let authorization = format!("OBS {}:{}", self.ak, signature);

        // 构造http请求
        let client = reqwest::Client::new();
        let res = client.put(url)
            .header("Content-MD5", &md5_string)
            .header("Date", &date)
            .header("Content-Type", file_type)
            .header("Content-Length", data.len())
            .header("Authorization", authorization)
            .body(data)
            .send()
            .await;

        let res = match res {
            Ok(response) => response,
            Err(e) => {
                return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e)));
            },
        };
        let _status: StatusCode = res.status();

        Ok(())

    }

    /// 上传文件
    pub async fn upload_file(&self, obj_key: &str, file_path: &str) -> Result<(), Box<dyn std::error::Error>> {
        let data = fs::read(file_path)?;
        self.upload_object(obj_key, data).await
    }

    /// 下载对象，方法内部会构造http请求：
    /// ```plain
    /// GET /{obj_key} HTTP/1.1
    /// Host: {bucket}.obs.cn-north-4.myhuaweicloud.com
    /// Date: {date}
    /// ```
    /// 
    pub async fn download_object(&self, obj_key: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        let obj_key = &Self::urlencode(obj_key);
        // 构造完整的url地址
        let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);

        //  获取GMT格式的时间字符串
        let date = utils::now_str_gmt();

        // 创建HmacSha1对象
        let hmacsha1 = HmacSha1();

        // 构造签名用的原始字符串
        let string_to_sign = hmacsha1.header_string_to_sign("GET", "", "", &date, "",  &format!("/{}/{}", self.bucket, obj_key));

        // 根据原始字符串+ak，获取header签名
        let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);

        // 构造请求头Authorization的值
        let authorization = format!("OBS {}:{}", self.ak, signature);

        // 构造http请求
        let client = reqwest::Client::new();
        let res = client.get(url)
            .header("Authorization", &authorization)
            .header("Date", &date)
            .send()
            .await?;

        // 如果请求成功，则返回字节内容
        if res.status().is_success() {
            return Ok(res.bytes().await?.to_vec());
        }
        
        Err(Box::new(Error::new(ErrorKind::Other, format!("请求失败，状态码={}", res.status()))))
    }

    /// 删除obs上的对象
    pub async fn delete_object(&self, obj_key: &str) -> Result<(), Box<dyn std::error::Error>> {
        let obj_key = &Self::urlencode(obj_key);
        // 构造完整的url地址
        let url = format!("https://{}.{}/{}", self.bucket, self.endpoint, obj_key);

        let md5_string = "";

        //  获取GMT格式的时间字符串
        let date = utils::now_str_gmt();

        // 创建HmacSha1对象
        let hmacsha1 = HmacSha1();

        // 构造签名用的原始字符串
        let string_to_sign = hmacsha1.header_string_to_sign("DELETE", &md5_string, "", &date, "",  &format!("/{}/{}", self.bucket, obj_key));

        // 根据原始字符串+ak，获取header签名
        let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);

        // 构造请求头Authorization的值
        let authorization = format!("OBS {}:{}", self.ak, signature);

        // 构造http请求
        let client = reqwest::Client::new();
        let res = client.delete(url)
            .header("Date", &date)
            .header("Authorization", authorization)
            .send()
            .await;

        let res = match res {
            Ok(response) => response,
            Err(e) => {
                return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e)));
            },
        };
        let status = res.status();
        println!("status = {}, {}", status, res.text_with_charset("utf-8").await?);

        Ok(())
    }

    /// 下载文件，并指定本地保存用的文件路径
    /// 
    /// # 参数
    /// 
    /// `overwrite` - 是否覆盖，true，当文件存在时，覆盖文件，false，当文件存在时，不覆盖文件
    /// 
    pub async fn download_file(&self, obj_key: &str, file_path: &str, overwrite: bool) -> Result<(), Box<dyn std::error::Error>> {
        let file_path = Path::new(file_path);

        // 判断文件是否存在，如果存在，不做任何操作
        if file_path.exists() && !overwrite {
            return Err(Box::new(Error::new(ErrorKind::AlreadyExists, "文件已存在，请删除文件或设置覆盖参数")));
        }

        // 根据父目录是否存在，选择性创建父目录
        let parent = file_path.parent().unwrap();
        if !parent.exists() {
            fs::create_dir_all(&parent)?;
        }

        // 下载文件，得到原始文件字节内容
        let data = self.download_object(obj_key).await?;
        
        // 保存文件
        fs::write(file_path, data)?;
        Ok(())
    }

    pub fn url_sign(&self, url_str: &str) -> Result<String, Box<dyn std::error::Error>> {
        let obs_object_url = Url::parse(url_str)?;
        let resource_part = obs_object_url.path();
        let host = obs_object_url.host().unwrap();
        let domain = match host {
            url::Host::Domain(domain) => domain.to_string(),
            _ => format!("{}.{}", self.bucket, self.endpoint)
        };
        let parts: Vec<&str> = domain.split(".").collect();
        let bucket_name = parts[0];

        let timestamp = utils::timestamp(Local::now(), 3600*2);

        //  获取GMT格式的时间字符串
        let expires = format!("{}", timestamp);

        // 创建HmacSha1对象
        let hmacsha1 = HmacSha1();

        // 构造签名用的原始字符串
        let string_to_sign = hmacsha1.url_string_to_sign("GET", "", "", &expires, "",  &format!("/{}{}", bucket_name, resource_part));

        // 根据原始字符串+ak，获取header签名
        let signature = hmacsha1.sign_to_base64string(&string_to_sign, &self.sk);
        let signature = form_urlencoded::byte_serialize(signature.as_bytes()).collect::<String>();

        // 构造url
        let sign_url = format!("{}?AccessKeyId={}&Expires={}&Signature={}", url_str, self.ak, expires, signature);
        Ok(sign_url)
    }

    fn urlencode(s: &str) -> String {
        let tokens: Vec<String> = s.split("/").map(|token| {
            encode(token).to_string()
        }).collect();
        tokens.join("/")
    }

}


/// obs对象的元数据信息
/// 
/// 这个结构体用于表示 OBS 对象的元数据，包含对象的各种属性，如名称、修改时间、内容标识、大小以及存储类型。
#[derive(Serialize, Deserialize, Debug)]
pub struct ObjectMeta {

    /// 对象名
    /// 
    /// 唯一标识 OBS 存储中的对象
    pub key: String,

    /// 对象最近一次被修改的时间（UTC时间）
    /// 
    /// 该时间戳表示对象在 OBS 存储中最后一次被修改的时刻，采用 UTC 时间格式。
    pub last_modified: String,

    /// 对象的base64编码的128位MD5摘要
    /// 
    /// 这个 ETag 值是对象内容的唯一标识，可以通过该值识别对象内容是否有变化。
    pub etag: String,

    /// 对象的字节数
    /// 
    /// 表示对象在存储中占用的字节大小
    pub size: u64,

    /// 对象的存储类型：STANDARD，WARM，COLD，DEEP_ARCHIVE
    /// 
    /// 不同的存储类型对应不同的存储成本和访问性能，用户可以根据对象的访问频率等因素选择合适的存储类型
    pub storage_class: String,
}

/// XML解析器
/// 
/// 用于解析XML格式的响应数据，目前这里面针对obs的接口“列举桶内对象”的响应结果进行解析，没有进行通用的xml解析，其不能作为通用工具使用
struct XmlParser { 
    xml: String,
}


impl XmlParser {
    fn new(xml: &str) -> Self {
        XmlParser { xml: xml.to_string() }
    }

    /// 解析obs接口“列举桶内对象”的响应结果
    /// 
    /// 该内部采用正则表达式进行解析，因此依赖外部的regex库
    fn parse(&self) -> Vec<ObjectMeta> {
        let xml = &self.xml;

        // 定义解析需要使用的正则表达式
        let contents_re = Regex::new(r#"<Contents>(.*?)</Contents>"#).unwrap();
        let key_regex = Regex::new(r#"<Key>(.*?)</Key>"#).unwrap();
        let last_modified_regex = Regex::new(r#"<LastModified>(.*?)</LastModified>"#).unwrap();
        let etag_regex = Regex::new(r#"<ETag>(.*?)</ETag>"#).unwrap();
        let size_regex = Regex::new(r#"<Size>(.*?)</Size>"#).unwrap();
        let storage_class_regex = Regex::new(r#"<StorageClass>(.*?)</StorageClass>"#).unwrap();


        // 解析Contents标签内的数据
        let mut contents_vec = Vec::new();
        for captures in contents_re.captures_iter(xml) {
            let inner_content = &captures[1];

            let key = key_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
            let last_modified = last_modified_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
            let etag = etag_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
            let size = size_regex.captures(inner_content).and_then(|cap| cap[1].parse().ok()).unwrap_or(0);
            let storage_class = storage_class_regex.captures(inner_content).map(|cap| cap[1].to_string()).unwrap_or_default();
            let content = ObjectMeta {
                key,
                last_modified,
                etag,
                size,
                storage_class,
            };
            contents_vec.push(content);
        }

        contents_vec
    }
}


#[cfg(test)]
mod tests {
    use super::*;
    use chrono::{Duration, Local};
    use std::time::{SystemTime, UNIX_EPOCH};

    #[test]
    fn test_parse_xml() {
        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><ListBucketResult xmlns="http://obs.myhwclouds.com/doc/2015-06-30/"><Name>obs-products</Name><Prefix>tmp</Prefix><Marker></Marker><MaxKeys>1000</MaxKeys><IsTruncated>false</IsTruncated><Contents><Key>tmp/</Key><LastModified>2024-12-03T12:01:48.020Z</LastModified><ETag>"d41d8cd98f00b204e9800998ecf8427e"</ETag><Size>0</Size><Owner><ID>74df55bf376f41d48959d2aa9deaaf38</ID></Owner><StorageClass>STANDARD</StorageClass></Contents><Contents><Key>tmp/index001.png</Key><LastModified>2025-08-20T07:42:59.813Z</LastModified><ETag>"de317c0b7b6e02b42ef2b9e29bb5906a"</ETag><Size>12082</Size><Owner><ID>74df55bf376f41d48959d2aa9deaaf38</ID></Owner><StorageClass>STANDARD</StorageClass></Contents><Contents><Key>tmp/index002.png</Key><LastModified>2025-08-20T07:52:10.204Z</LastModified><ETag>"de317c0b7b6e02b42ef2b9e29bb5906a"</ETag><Size>12082</Size><Owner><ID>74df55bf376f41d48959d2aa9deaaf38</ID></Owner><StorageClass>STANDARD</StorageClass></Contents></ListBucketResult>"#;
        let parser = XmlParser::new(xml);
        let contents = parser.parse();
        let json_data = serde_json::to_string_pretty(&contents).unwrap();
        println!("{}", json_data);
    }

    #[test]
    fn test_timestamp() {
        let now = Local::now();
        let two_hours = Duration::hours(2);
        let future_time = now + two_hours;

        let system_time: SystemTime = future_time.into();
        let duration = system_time.duration_since(UNIX_EPOCH).unwrap();
        let timestamp = duration.as_secs();
        println!("timestamp = {}", timestamp);
    }

}